#ifndef __MY_MATRIX_H__
#define __MY_MATRIX_H__

/******************************************************************************
**    INCLUDES
******************************************************************************/
#include <initializer_list>
#include <utility>
#include <functional>
#include <sstream>
#include <stdexcept>
#include <algorithm>

/******************************************************************************
**    MACROS
******************************************************************************/


/******************************************************************************
**    TYPE DEFINITIONS
******************************************************************************/
template<typename T>
class Matrix;
	
/******************************************************************************
**    CLASSES/FUNCTIONS DEFINITIONS
******************************************************************************/
namespace matrix {
template <typename T>
void CheckShapeMatches(const Matrix<T>& lhs, const Matrix<T>& rhs)
{
    if (lhs.row_size() != rhs.row_size() ||
        lhs.column_size() != rhs.column_size()) 
    {
        std::stringstream ss;
        ss << "Matrices dimensions don't match: "
            << "(" << lhs.row_size() << ", " << lhs.column_size() << ") vs "
            << "(" << rhs.row_size() << ", " << rhs.column_size() << ")";
        throw std::runtime_error(ss.str());
    }
}

template <typename T>
void CheckShapeMultipliable(const Matrix<T>& lhs, 
                                      const Matrix<T>& rhs) 
{
    if (lhs.column_size() != rhs.row_size()) 
    {
        std::stringstream ss;
        ss << "Matrices dimensions not multipliable: "
           << "(" << lhs.row_size() << ", " << lhs.column_size() << ") vs "
           << "(" << rhs.row_size() << ", " << rhs.column_size() << ")";
        throw std::runtime_error(ss.str());
    }
}

template <typename T>
void CheckAugmentable(const Matrix<T>& coef, const Matrix<T>& extra) 
{
    if (coef.row_size() != extra.row_size()) 
    {
        std::stringstream ss;
        ss << "Equal row size expected, actual "
            << "(" << coef.row_size() << ", " << coef.column_size() << ") vs "
            << "(" << extra.row_size() << ", " << extra.column_size() << ")";
        throw std::runtime_error(ss.str());
    }
}


template <typename T>
void Print(const Matrix<T>& mat, std::ostream* os) {
    typedef typename Matrix<T>::size_type size_type;
    for (size_type row = 0; row < mat.row_size(); ++row) {
        for (size_type column = 0; column < mat.column_size(); ++column) {
            (*os) << mat[row][column] << " ";
        }
        (*os) << "\n";
    }
}

template <typename T>
void PrintAugmented(const Matrix<T>& coef, const Matrix<T>& extra,
                          std::ostream* os) 
{
    CheckAugmentable(coef, extra);
    typedef typename Matrix<T>::size_type size_type;
    for (size_type row = 0; row < coef.row_size(); ++row) 
    {
        for (size_type column = 0; column < coef.column_size(); ++column) 
        {
            (*os) << coef[row][column] << " ";
        }
        
        (*os) << "| ";
        
        for (size_type column = 0; column < extra.column_size(); ++column) 
        {
            (*os) << extra[row][column] << " ";
        }
        
        (*os) << "\n";
    }
}

} /** end of namespace matrix */

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

template<typename T>
class Matrix
{
public:
    typedef T value_type;
    typedef T& value_ref; /** reference */
    typedef const T& const_value_ref;
    typedef T* value_ptr; /** a pointer for value */
    typedef std::size_t size_type;
    typedef T* row_type;
    typedef const T* const_row_type;

    typedef std::initializer_list<std::initializer_list<value_type>> 
                                                        value_list_type;
    
    typedef row_type* data_type;


    /**first: row size, second: column size */
    typedef std::pair<size_type, size_type> shape_type;

    typedef std::function<bool(const value_type&, const value_type&)> 
                                                    value_compare_func;


public:
    /** Constructors and Destructor */
    Matrix(size_type row_size, size_type column_size,
           const value_type& filled = value_type());
        
    Matrix(value_list_type il);
    Matrix();
    Matrix(const Matrix& other);
    Matrix(Matrix&& other);

    ~Matrix();
public:
    /** size and shape */
    size_type row_size() const;
    size_type column_size() const;
    size_type size() const;
    shape_type shape() const;
    bool empty() const;

public:
    /** Assignment operator */
    Matrix & operator=(const Matrix& other);
    Matrix& operator=(Matrix&& other);

public:
    /** get value */
    value_ref operator()(size_type row, size_type column);
    const_value_ref operator()(size_type row, size_type column) const;
    
    /** Supports access pattern `mat[row][column]`. **/
    row_type operator[](size_type row);
    const_row_type operator[](size_type row) const;

public:
    /** resize */
    void row_resize(size_type new_row_size, 
                        const value_type& filled = value_type());
                        
    void column_resize(size_type new_column_size, 
                             const value_type& filled = value_type());
                             
    void reshape(size_type row_size, size_type column_size, 
                    const value_type& filled = value_type());

public:
    /** Arithmetric operators */
    Matrix& operator+=(const Matrix& other); /** add */
    Matrix& operator-=(const Matrix& other);  /** minus */

    /** multiply */
    Matrix& operator*=(const value_type& scaler); 
    Matrix& operator*=(const Matrix& other);
    friend Matrix operator*(const Matrix& lhs, Matrix&& rhs)
    {
       // matrix::CheckShapeMultipliable(lhs, rhs);
        Matrix<T> result(lhs);

        result *= rhs; /** todo refine me?? */
        return result;
    }
    
    /** Each element corresponds to multiply */
    Matrix& multiply(const Matrix& other); 

    Matrix& operator/=(const value_type& scaler); /** div */
    Matrix& operator%=(const value_type& scaler); /** mod */

    /** compare */
    bool compare(const Matrix& other, 
                    value_compare_func value_compare = 
                    std::equal_to<value_type>()) const;


private:
    /** check functions */
    void checkColumnSizesEqual(value_list_type il);
    void checkShapeRange(size_type row, size_type column) const;
    void checkShapeMatches(const Matrix<T>& other);

    /** for process data pointer */
    void initializeData(size_type rowSize, size_type columnSize);
    void initializeData(size_type rowSize, size_type columnSize, 
                              const value_type& filled);
                              
    void uninitializeData();
    void releaseData(data_type data);

    value_type inner_product(value_type* beg1, value_type* end1, 
                                   data_type beg2, size_type column2, 
                                   value_type init);
private:
    data_type mData = nullptr;
    size_type mRowSize = 0;
    size_type mColumnSize = 0;
};

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/** Constructors and (default) Destructor */
template<typename T>
Matrix<T>::Matrix(size_type row_size, size_type column_size,
                  const value_type& filled /*= value_type()*/)
{
    if (row_size && column_size)
    {
        initializeData(row_size, column_size, filled);
    }
}

template<typename T>
Matrix<T>::Matrix(value_list_type il)
{   
    if (il.size() > 0)
    {
        checkColumnSizesEqual(il);
        initializeData(il.size(), il.begin()->size());

        size_type rowIdx = 0;
        size_type columnIdx = 0;        

        for (auto row_il : il)
        {
            columnIdx = 0;
            for (auto val : row_il)
            {
                mData[rowIdx][columnIdx] = val;
                columnIdx++;
            }

            rowIdx++;
        }
    }
}

template<typename T>
Matrix<T>::Matrix()
{
    /**todo something */
}

template<typename T>
Matrix<T>::Matrix(const Matrix& other)
{
    if (other.mRowSize && other.mColumnSize && other.mData)
    {

        initializeData(other.mRowSize, other.mColumnSize);
        memcpy(mData[0], other.mData[0], 
               mRowSize * mColumnSize * sizeof(value_type));
    }
}

template<typename T>
Matrix<T>::Matrix(Matrix&& other)
{
    if (other.mRowSize && other.mColumnSize && other.mData)
    {
        mRowSize = other.mRowSize;
        mColumnSize = other.mColumnSize;
        mData = other.mData;

        other.mRowSize = 0;
        other.mColumnSize = 0;
        other.mData = nullptr;
    }
}

template<typename T>
Matrix<T>::~Matrix()
{
    uninitializeData();
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/** size and shape */
template<typename T>
typename Matrix<T>::size_type Matrix<T>::row_size() const
{
    return mRowSize;
}

template<typename T>
typename Matrix<T>::size_type Matrix<T>::column_size() const
{
    return mColumnSize;
}

template<typename T>
typename Matrix<T>::size_type Matrix<T>::size() const
{
    return mRowSize * mColumnSize;
}

template<typename T>
typename Matrix<T>::shape_type Matrix<T>::shape() const
{
    return std::make_pair(mRowSize, mColumnSize);
}

template<typename T>
bool Matrix<T>::empty() const
{
    return (mRowSize == 0) || (mColumnSize == 0);
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/** Assignment operator */

template<typename T>
Matrix<T>& Matrix<T>::operator=(const Matrix<T>& other)
{
    if (this != &other)
    {
        if (other.mRowSize && other.mColumnSize && other.mData)
        {
            if ((mRowSize != other.mRowSize) || 
                (mColumnSize != other.mColumnSize))
            {
                uninitializeData();
                initializeData(other.mRowSize, other.mColumnSize);
            
            }
        
            memcpy(mData[0], other.mData[0], 
                   mRowSize * mColumnSize * sizeof(value_type));
        }
        else
        {
            uninitializeData();
        }

    }

    return *this;
}

template<typename T>
Matrix<T>& Matrix<T>::operator=(Matrix<T>&& other)
{
    if (this != &other)
    {
        mRowSize = other.mRowSize;
        mColumnSize = other.mColumnSize;
        mData = other.mData;

        other.mRowSize = 0;
        other.mColumnSize = 0;
        other.mData = nullptr;
    }

    return *this;
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/** get value */
template<typename T>
typename Matrix<T>::value_ref Matrix<T>::operator()(size_type row, 
                                                        size_type column)
{
    checkShapeRange(row, column);
    return mData[row][column];
}


template<typename T>
typename Matrix<T>::const_value_ref Matrix<T>::operator()(size_type row, 
                                                        size_type column) const
{
    checkShapeRange(row, column);
    return mData[row][column];
}

/** Supports access pattern `mat[row][column]`. **/

template<typename T>
typename Matrix<T>::row_type Matrix<T>::operator[](size_type row)
{
    return mData[row];
}


template<typename T>
typename Matrix<T>::const_row_type Matrix<T>::operator[](
                                                        size_type row) const
{
    return mData[row];
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/** resize */

template<typename T>
void Matrix<T>::row_resize(size_type new_row_size, 
                               const value_type& filled /*= value_type()*/)
{
    if (new_row_size <= mRowSize)
    {
        mRowSize = new_row_size;
    }
    else
    {
        data_type orgData = mData;
        size_type orgSize = mRowSize * mColumnSize ;
        initializeData(new_row_size, mColumnSize);

        memcpy(mData[0], orgData[0], orgSize * sizeof(value_type));

        size_type newSize = mRowSize * mColumnSize;
        value_type* pData = mData[0] + orgSize;

        for (size_t i = 0; i < newSize - orgSize; i++)
        {
            pData[i] = filled;
        }

        releaseData(orgData);
    }
}


template<typename T>
void Matrix<T>::column_resize(size_type new_column_size, 
                                    const value_type& filled /*= value_type()*/)
{
    if (new_column_size <= mColumnSize)
    {
        mColumnSize = new_column_size;
    }
    else
    {
        data_type orgData = mData;
        size_type orgColumnSize = mColumnSize;
        initializeData(mRowSize, new_column_size);


        for (size_t row = 0; row < mRowSize; row++)
        {
            for (size_t column = 0; column < mColumnSize; column++)
            {
                if (column < orgColumnSize)
                {
                    mData[row][column] = orgData[row][column];
                }
                else
                {
                    mData[row][column] = filled;
                }
            }

        }

        releaseData(orgData);
    }
}

template<typename T>
void Matrix<T>::reshape(size_type row_size, size_type column_size, 
                           const value_type& filled /*= value_type()*/)
{
    if ((row_size <= mRowSize) && (column_size <= mColumnSize))
    {
        mRowSize = row_size;
        mColumnSize = column_size;
    }
    else if((row_size == mRowSize) && (column_size != mColumnSize))
    {
        column_resize(column_size, filled);
    }
    else if ((column_size == mColumnSize) && (row_size != mRowSize))
    {
        row_resize(row_size, filled);
    }
    else
    {/** row and column both bigger then old size */
        data_type orgData = mData;
        size_type orgRowSize = mRowSize;
        size_type orgColumnSize = mColumnSize;
        initializeData(row_size, column_size);


        for (size_t i = 0; i < mRowSize; i++)
        {
            if (i < orgRowSize)
            {
                for (size_t j = 0; j < mColumnSize; j++)
                {
                    if (j < orgColumnSize)
                    {
                        mData[i][i] = orgData[i][j];
                    }
                    else
                    {
                        mData[i][j] = filled;
                    }
                }
            }
            else
            {
                for (size_t j = 0; j < mColumnSize; j++)
                {
                    mData[i][j] = filled;
                }
            }
        }

        releaseData(orgData);
    }
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/** Arithmetric operators */
template<typename T>
Matrix<T>& Matrix<T>::operator+=(const Matrix& other) /** add */
{
    matrix::CheckShapeMatches(*this, other);
    data_type otherData = other.mData;

    for (size_t row = 0; row < mRowSize; row++)
    {
        for (size_t column = 0; column < mColumnSize; column++)
        {
            mData[row][column] += otherData[row][column];
        }
    }

    return *this;
}

template<typename T>
Matrix<T>& Matrix<T>::operator-=(const Matrix& other)  /** minus */
{
    matrix::CheckShapeMatches(*this, other);
    data_type otherData = other.mData;

    for (size_t row = 0; row < mRowSize; row++)
    {
        for (size_t column = 0; column < mColumnSize; column++)
        {
            mData[row][column] -= otherData[row][column];
        }
    }

    return *this;
}


/** multiply */
template<typename T>
Matrix<T>& Matrix<T>::operator*=(const value_type& scaler)
{

    for (size_t row = 0; row < mRowSize; row++)
    {
        for (size_t column = 0; column < mColumnSize; column++)
        {
            mData[row][column] *= scaler;
        }
    }

    return *this;
}


template<typename T>
Matrix<T>& Matrix<T>::operator*=(const Matrix& other)
{
    matrix::CheckShapeMultipliable(*this, other);
    data_type orgData = mData;
    size_type orgColumnSize = mColumnSize;

    initializeData(mRowSize, other.mColumnSize);

    for (size_t row = 0; row < mRowSize; row++)
    {
        for (size_t column = 0; column < mColumnSize; column++)
        {
            mData[row][column] = inner_product(&(orgData[row][0]),
                &(orgData[row][orgColumnSize]),
                other.mData, column, value_type());
            
        }
    }

    releaseData(orgData);

    return *this;
}



/** Each element corresponds to multiply */
template<typename T>
Matrix<T>& Matrix<T>::multiply(const Matrix<T>& other) 
{
    matrix::CheckShapeMatches(*this, other);
    data_type otherData = other.mData;

    for (size_t row = 0; row < mRowSize; row++)
    {
        for (size_t column = 0; column < mColumnSize; column++)
        {
            mData[row][column] *= otherData[row][column];
        }
    }

    return *this;
}

template<typename T>
Matrix<T>& Matrix<T>::operator/=(const value_type& scaler) /** div */
{
    for (size_t row = 0; row < mRowSize; row++)
    {
        for (size_t column = 0; column < mColumnSize; column++)
        {
            mData[row][column] /= scaler;
        }
    }

    return *this;
}


template<typename T>
Matrix<T>& Matrix<T>::operator%=(const value_type& scaler) /** mod */
{
    for (size_t row = 0; row < mRowSize; row++)
    {
        for (size_t column = 0; column < mColumnSize; column++)
        {
            mData[row][column] %= scaler;
        }
    }

    return *this;
}

/** compare */
template<typename T>
bool Matrix<T>::compare(const Matrix<T>& other, 
                          value_compare_func value_compare
                          /* = std::equal_to<value_type>()*/) const
{
    bool ret = false;
    
    if ((mRowSize == other.mRowSize) && (mColumnSize == other.mColumnSize))
    {
        ret = true;
        data_type otherData = other.mData;
        for (size_t row = 0; row < mRowSize; row++)
        {
            for (size_t column = 0; column < mColumnSize; column++)
            {
                if (!value_compare(mData[row][column], otherData[row][column]))
                {
                    ret = false;
                    break;
                }
            }
        }
    }

    return ret;
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/** private member function */
template<typename T>
void Matrix<T>::checkColumnSizesEqual(value_list_type il)
{
    auto iter = std::adjacent_find(
        il.begin(), il.end(),
        [](std::initializer_list<T> lhs, std::initializer_list<T> rhs) {
        return lhs.size() != rhs.size();
        });

    if (iter != il.end())
    {
        auto diff = iter + 1;// std::adjacent_find() tells us *iter != *(iter+1)
        std::stringstream ss;
        ss << "Each row should have the same column size: "
            << "row " << (diff - il.begin()) 
            << "'s column size " << diff->size()
            << " vs previous rows' column size " << iter->size();
        throw std::invalid_argument(ss.str());
    }
}

template<typename T>
void Matrix<T>::checkShapeRange(size_type row, size_type column) const
{
    if ((row >= mRowSize) || (column >= mColumnSize))
    {
        std::stringstream ss;
        ss << "Matrix shape (" << row << ", " << column << ") out of range "
            << "(" << mRowSize << ", " << mColumnSize << ")";
        throw std::out_of_range(ss.str());
    }
}

template<typename T>
void Matrix<T>::checkShapeMatches(const Matrix<T>& other)
{
    if ((mRowSize != other.mRowSize) ||
        (mColumnSize != other.mColumnSize))
    {
        std::stringstream ss;
        ss << "Matrices shape don't match: "
            << "(" << mRowSize << ", " << mColumnSize << ") vs "
            << "(" << other.mRowSize << ", " << other.mColumnSize << ")";
        throw std::runtime_error(ss.str());
    }
}


template<typename T>
void Matrix<T>::initializeData(size_type rowSize, size_type columnSize)
{
    mRowSize = rowSize;
    mColumnSize = columnSize;
    mData = new row_type[rowSize];
    
    value_ptr pData = new  value_type[rowSize * columnSize];


    for (size_type i = 0; i < rowSize; i++)
    {
        mData[i] = pData + i * columnSize;
    }
}

template<typename T>
void Matrix<T>::initializeData(size_type rowSize, size_type columnSize, 
                                     const value_type& filled)
{
    initializeData(rowSize, columnSize);

    std::fill(mData[0], mData[0] + mRowSize * mColumnSize, filled);
}

template<typename T>
void Matrix<T>::uninitializeData()
{

    releaseData(mData);

    mData = nullptr;
    mRowSize = 0;
    mColumnSize = 0;
}

template<typename T>
void Matrix<T>::releaseData(data_type data)
{
    if (data)
    {
        row_type pRow = data[0];
        delete[] data;
        delete[] pRow;
    }
}

template<typename T>
typename Matrix<T>::value_type Matrix<T>::inner_product(value_type* beg1, 
                                                            value_type* end1, 
                                                            data_type beg2, 
                                                            size_type column2, 
                                                            value_type value)
{
    size_type rowIdx = 0;
    while (beg1 != end1) 
    {
        value = std::move(value) + *beg1 * beg2[rowIdx][column2];
        ++beg1;
        ++rowIdx;
    }

    return value;
}


///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////


/**  Arithmetric operators */

/** add */
template <typename T>
Matrix<T> operator+(const Matrix<T>& lhs, const Matrix<T>& rhs) 
{
    matrix::CheckShapeMatches(lhs, rhs);
    Matrix<T> result(lhs);
    return result += rhs;
}


template <typename T>
Matrix<T> operator+(const Matrix<T>& lhs, Matrix<T>&& rhs)
{
    matrix::CheckShapeMatches(lhs, rhs);
    Matrix<T> result(std::move(rhs));

    result += lhs;
    return result;
}


template <typename T>
Matrix<T> operator+(Matrix<T>&& lhs, const Matrix<T>& rhs)
{
    return rhs + std::move(lhs);
}

/** minus */
template <typename T>
Matrix<T> operator-(const Matrix<T>& lhs, const Matrix<T>& rhs)
{
    matrix::CheckShapeMatches(lhs, rhs);
    Matrix<T> result(lhs);
    return result -= rhs;
}


template <typename T>
Matrix<T> operator-(const Matrix<T>& lhs, Matrix<T>&& rhs)
{
    matrix::CheckShapeMatches(lhs, rhs);
    Matrix<T> result(std::move(rhs));

    for (size_t row = 0; row < lhs.row_size(); row++)
    {
        for (size_t column = 0; column < lhs.column_size(); column++)
        {
            result[row][column] = lhs[row][column] - result[row][column];
        }
    }
    
    return result;
}


template <typename T>
Matrix<T> operator-(Matrix<T>&& lhs, const Matrix<T>& rhs) 
{
    matrix::CheckShapeMatches(lhs, rhs);
    Matrix<T> result(std::move(lhs));
    result -= rhs;
    return result;
}

/** multiply */
template <typename T>
Matrix<T> operator*(const Matrix<T>& mat, const T& scaler) 
{
    Matrix<T> result(mat);
    return result *= scaler;
}

template <typename T>
Matrix<T> operator*(const T& scaler, const Matrix<T>& mat)
{
    return mat * scaler;
}

template <typename T>
Matrix<T> operator*(Matrix<T>&& mat, const T& scaler) 
{
    Matrix<T> result(std::move(mat));
    result *= scaler;
    return result;
}

template <typename T>
Matrix<T> operator*(const T& scaler, Matrix<T>&& mat) 
{
    return std::move(mat) * scaler;
}

template <typename T>
Matrix<T> operator*(const Matrix<T>& lhs, const Matrix<T>& rhs)
{
    Matrix<T> result(lhs);
    return result *= rhs;
}

template <typename T>
Matrix<T> operator*(Matrix<T>&& lhs, const Matrix<T>& rhs)
{
    Matrix<T> result(std::move(lhs));
    result *= rhs;
    return result;
}

/** div */
template <typename T>
Matrix<T> operator/(const Matrix<T>& mat, const T& scaler) 
{
    Matrix<T> result(mat);
    return result /= scaler;
}

template <typename T>
Matrix<T> operator/(Matrix<T>&& mat, const T& scaler)
{
    Matrix<T> result(std::move(mat));
    result /= scaler;
    return result;
}

/** mod */
template <typename T>
Matrix<T> operator%(const Matrix<T>& mat, const T& scaler)
{
    Matrix<T> result(mat);
    return result %= scaler;
}

template <typename T>
Matrix<T> operator%(Matrix<T>&& mat, const T& scaler)
{
    Matrix<T> result(std::move(mat));
    result %= scaler;
    return result;
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/** Comparasion operators */

template <typename T>
bool operator==(const Matrix<T>& lhs, const Matrix<T>& rhs)
{
    return lhs.compare(rhs);
}

template <typename T>
bool operator!=(const Matrix<T>& lhs, const Matrix<T>& rhs) 
{
    return !(lhs == rhs);
}


#endif // !__MY_MATRIX_H__

